.. code:: ipython3 import pandas as pd from seeq import spy # Set the compatibility option so that you maximize the chance that SPy will remain compatible with your notebook/script spy.options.compatibility = 192 .. code:: ipython3 # Log into Seeq Server if you're not using Seeq Data Lab: spy.login(url='http://localhost:34216', credentials_file='../credentials.key', force=False) Workbook Templates ================== A *workbook* is either a Workbench Analysis or an Organizer Topic. You create workbooks in SPy in a variety of ways: - When you execute :doc:`spy.push() `, a workbook will be created and the signals/conditions pushed will be scoped to that workbook. - You can execute :doc:`spy.workbooks.push() ` to push workbooks that have been saved to disk. - You can associate workbooks with asset trees that have been defined using :doc:`spy.assets `. The easiest way to define the configuration and content of a workbook is by using the Workbench and Organizer user interface to create *template workbooks*. Creating a Template ------------------- To create an Analysis template, you simply go into Workbench and create a workbook and as many worksheets as you need to define the template. Add “placeholder” items to the worksheet and arrange/configure the worksheet in whatever way makes sense for your use case. You can make entries in the worksheet’s journal as well, including images and workstep links. The ``sinusoid()`` function (using the Formula Tool) is a great way to create placeholders, because you can easily tailor it to the range and units you expect to see after you’ve substituted in real data in the steps that follow. Once you’re finished configuring the workbooks and worksheets, you will use SPy to save the workbook to disk so that you can later use it as a template. Just copy the URL from the browser’s address bar and replace the value of the ``url`` variable in the code below: :: url = 'http://localhost:34216/workbook/3EA21D97-782B-472B-B065-2DC76ECC152F/worksheet/4ED5F9B9-D82F-4017-ACEE-0EBBD1CC4E13' workbooks = spy.workbooks.pull(url) spy.workbooks.save(workbooks, 'My First Workbook Template') This will save the workbook files to a folder on disk called ``My First Workbook Template`` and you’ll see how we load it below. Loading a Template ------------------ Now let’s say you want to use the template to control the workbook that is created when you call ``spy.push()``. In this example, we’ll use a template that’s built into this SPy documentation notebook. As you can see in the screenshot below, the workbook has been configured with several worksheets. The first is called “Trend Template” and has three items in it. Notice how the Journal entry has text that is surrounded by double-curly-braces, like ``{{favorite color}}``. This is referred to as a *mustache variable* (named after the popular `Mustache templating system `__). These variables can be replaced by values you specify in Python before you push, as you will see further below. .. image:: AnalysisTemplateScreenshot.jpg .. code:: ipython3 # Load the template shown above (included in SPy for the purposes of this example). # Note how the `as_template_with_label` argument is used here, which is the key to establishing the loaded # workbooks as templates instead of regular workbook objects. The specified label must be set to something # unique across the entire Seeq service so that there aren't any identifier conflicts when pushing. If # you're unsure, just include your name and that will usually be unique. For convenience in this example, # we will use the logged-in user's name. workbooks = spy.workbooks.load('./Support Files/Workbook Templates.zip', as_template_with_label=f'{spy.user.username} Workbook Template Example 1') # Grab the Analysis Workbook from the list analysis_template = workbooks['Analysis Template'] # Retrieve a specific analysis worksheet from the list of returned workbooks worksheet = analysis_template.worksheets['Trend Template'] worksheet .. code:: ipython3 workbooks When you load a workbook as a template, it exposes a ``code`` attribute that tells you how to plug specific items into the template. These are referred to as the *parameters*. .. code:: ipython3 print(worksheet.code) .. parsed-literal:: worksheet.parameters = { "D2C089B6-CE85-46FC-8392-E11CC0C08336 [Signal] Area A_Compressor Stage": None, "9564A6B8-8A8F-4F6D-AC63-00EA38962B7A [Signal] Area A_Temperature": None, "DCED9C36-A4BE-4783-9216-DC06B3F57D8C [Signal] Area A_Compressor Power": None, "Temperature Journal Link Text": None, "Compressor Power Journal Link Text": None, "Compressor Stage Journal Link Text": None, "favorite color": None } This worksheet contains three display items that can be replaced by items of our choosing, as well as several Mustache variables from the Journal. You will see below how we replace the ``None`` values with the items we want. Pushing with a Template Worksheet --------------------------------- First let’s create a metadata DataFrame with some sinusoids: .. code:: ipython3 metadata_df = pd.DataFrame([{ 'Name': 'Sinusoid 1', 'Type': 'Signal', 'Formula': 'sinusoid(1h)' }, { 'Name': 'Sinusoid 2', 'Type': 'Signal', 'Formula': 'sinusoid(2h)' }, { 'Name': 'Sinusoid 3', 'Type': 'Signal', 'Formula': 'sinusoid(3h)' }]) Next, we will copy and paste the code block that we printed earlier into a real code cell so that we can set the parameters appropriately. In this example, we’re just specifying the ``Name`` of the signals (replacing the ``None`` placeholders), but we could have supplied ``ID``, ``Type``, ``Path`` and/or ``Asset`` values that correspond to the row being pushed. (It can also be a one-row DataFrame instead of a dictionary.) .. code:: ipython3 worksheet.parameters = { "9564A6B8-8A8F-4F6D-AC63-00EA38962B7A [Signal] Area A_Temperature": {'Name': 'Sinusoid 1'}, "DCED9C36-A4BE-4783-9216-DC06B3F57D8C [Signal] Area A_Compressor Power": {'Name': 'Sinusoid 2'}, "D2C089B6-CE85-46FC-8392-E11CC0C08336 [Signal] Area A_Compressor Stage": {'Name': 'Sinusoid 3'}, "Temperature Journal Link Text": 'SINUSOID 1', "Compressor Power Journal Link Text": 'SINUSOID 2', "Compressor Stage Journal Link Text": 'SINUSOID 3', "favorite color": 'blue' } # Give the worksheet a different name than "Trend Template" worksheet.name = 'My Trend!' Now we push the metadata DataFrame and pass in the worksheet argument: .. code:: ipython3 spy.push(metadata=metadata_df, workbook='Workbook Template Example 1', worksheet=worksheet) When you click on the link to the workbook/worksheet above, you should see the following: .. image:: AnalysisTemplateScreenshot2.jpg Notice that all of the items have been replaced with the things we pushed, and the mustache variables in the Journal have been replaced by the values we specified in the ``worksheet.parameters`` above. Note: Both Journals and Organizer Topics are backed by HTML markup. If you want to supply HTML as a parameter for one of the mustache variables, you must declare the variable as ``{{&Variable Name}}``. That way the HTML will not be “escaped” and be shown to the user literally. Pushing with a Template Workbook -------------------------------- You can also push an entire workbook instead of just a single worksheet. Following much the same pattern, you will print the parameters code for the workbook like so: .. code:: ipython3 workbooks = spy.workbooks.load('./Support Files/Workbook Templates.zip', as_template_with_label=f'{spy.user.username} Workbook Template Example 2') .. code:: ipython3 # Retrieve a specific workbook from the list of returned workbooks workbook = workbooks['Analysis Template'] print(workbook.code) .. parsed-literal:: workbook.parameters = { "D2C089B6-CE85-46FC-8392-E11CC0C08336 [Signal] Area A_Compressor Stage": None, "9564A6B8-8A8F-4F6D-AC63-00EA38962B7A [Signal] Area A_Temperature": None, "DCED9C36-A4BE-4783-9216-DC06B3F57D8C [Signal] Area A_Compressor Power": None, "Temperature Journal Link Text": None, "Compressor Power Journal Link Text": None, "Compressor Stage Journal Link Text": None, "favorite color": None, "8A4F0E26-8A0C-4127-9E11-B67E031C6049 [Signal] Example >> Cooling Tower 1 >> Area A >> Temperature": None, "D16FC368-AE8E-47B4-B1A3-9C2A3FBA2BB6 [Asset] Example >> Cooling Tower 1": None, "4B40EAFC-91ED-4AB0-8199-F21AF40A8350 [Asset] Example >> Cooling Tower 1 >> Area A": None, "07F3161F-6644-4505-BC33-16D6155B004E [Condition] Hot": None } Again, we will create a DataFrame full of signals, but this time we’ll put them in an asset tree so we can illustrate how to properly populate a Treemap template. .. code:: ipython3 metadata_df = pd.DataFrame([ {'Path': 'Waveforms', 'Asset': 'Waveforms 1', 'Name': 'Sinusoid', 'Type': 'Signal', 'Formula': 'sinusoid(1h)'}, {'Path': 'Waveforms', 'Asset': 'Waveforms 1', 'Name': 'Sawtooth', 'Type': 'Signal', 'Formula': 'sawtooth(1h)'}, {'Path': 'Waveforms', 'Asset': 'Waveforms 1', 'Name': 'Square', 'Type': 'Signal', 'Formula': 'squareWave(1h)'}, {'Path': 'Waveforms', 'Asset': 'Waveforms 1', 'Name': 'Hours', 'Type': 'Condition', 'Formula': 'hours()'}, {'Path': 'Waveforms', 'Asset': 'Waveforms 2', 'Name': 'Sinusoid', 'Type': 'Signal', 'Formula': 'sinusoid(12h, 15V)'}, {'Path': 'Waveforms', 'Asset': 'Waveforms 2', 'Name': 'Sawtooth', 'Type': 'Signal', 'Formula': 'sawtooth(12h, 15V)'}, {'Path': 'Waveforms', 'Asset': 'Waveforms 2', 'Name': 'Square', 'Type': 'Signal', 'Formula': 'squareWave(12h, 15V)'}, {'Path': 'Waveforms', 'Asset': 'Waveforms 2', 'Name': 'Hours', 'Type': 'Condition', 'Formula': 'hours()'}, ]) .. code:: ipython3 # Provide a name for the pushed workbook. Unlike the previous example where the workbook name was specified # as part of the push arguments, in this case we override it on the template object. You can override any # property of a workbook/worksheet prior to pushing. workbook.name = 'Workbook Template Example 2' workbook.parameters = { "DCED9C36-A4BE-4783-9216-DC06B3F57D8C [Signal] Area A_Compressor Power": {'Asset': 'Waveforms 1', 'Name': 'Sinusoid'}, "9564A6B8-8A8F-4F6D-AC63-00EA38962B7A [Signal] Area A_Temperature": {'Asset': 'Waveforms 1', 'Name': 'Sawtooth'}, "D2C089B6-CE85-46FC-8392-E11CC0C08336 [Signal] Area A_Compressor Stage": {'Asset': 'Waveforms 1', 'Name': 'Square'}, "Temperature Journal Link Text": 'Sawtooth', "Compressor Power Journal Link Text": 'Sinusoid', "Compressor Stage Journal Link Text": 'Square', "favorite color": 'beige', # This is the condition used to color the treemap "07F3161F-6644-4505-BC33-16D6155B004E [Condition] Hot": {'Asset': 'Waveforms 1', 'Name': 'Hours'}, # This is the signal used for the Maximum statistic in the treemap and its parent assent "8A4F0E26-8A0C-4127-9E11-B67E031C6049 [Signal] Example >> Cooling Tower 1 >> Area A >> Temperature": {'Asset': 'Waveforms 1', 'Name': 'Sawtooth'}, "4B40EAFC-91ED-4AB0-8199-F21AF40A8350 [Asset] Example >> Area A": {'Asset': 'Waveforms 1', 'Type': 'Asset'}, # This is the parent asset that will be used in the Treemap worksheet "D16FC368-AE8E-47B4-B1A3-9C2A3FBA2BB6 [Asset] Cooling Tower 1": {'Asset': 'Waveforms', 'Type': 'Asset'} } Now when we push, we’ll pass the ``workbook`` argument instead of the ``worksheet`` argument. .. code:: ipython3 pushed_df = spy.push(metadata=metadata_df, workbook=workbook) After pushing, the link will take you to a workbook that has four worksheets whose contents correspond to the template parameters specified above: .. image:: AnalysisTemplateScreenshot3.jpg Using Pre-existing Signals/Conditions/Scalars etc ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In the examples above, we are both creating items (signals/conditions/scalars/etc) and using them in a template – all in one call to ``spy.push()``. If the items are already created, then you can use ``spy.workbooks.push()`` instead, like so: .. code:: ipython3 # Retrieve Area B items from the Example Data asset tree area_b_signals = spy.search({'Path': 'Example >> Cooling Tower 1', 'Asset': 'Area B'}) workbooks = spy.workbooks.load('./Support Files/Workbook Templates.zip', as_template_with_label=f'{spy.user.username} Workbook Template Example 2.1') workbook = workbooks['Analysis Template'] workbook.name = 'Workbook Template Example 2.1' workbook.parameters = { "DCED9C36-A4BE-4783-9216-DC06B3F57D8C [Signal] Area A_Compressor Power": # You can specify a (single) DataFrame row as long as it contains a valid ID column area_b_signals[area_b_signals['Name'] == 'Compressor Power'], "9564A6B8-8A8F-4F6D-AC63-00EA38962B7A [Signal] Area A_Temperature": area_b_signals[area_b_signals['Name'] == 'Temperature'], "D2C089B6-CE85-46FC-8392-E11CC0C08336 [Signal] Area A_Compressor Stage": # You can specify an ID directly as a string area_b_signals[area_b_signals['Name'] == 'Compressor Stage'].iloc[0]['ID'] } spy.workbooks.push(workbook) When you click the link to examine the pushed workbook, you’ll see that the ``Area A_Xxxxx`` signals have been replaced with equivalent ``Example >> Cooling Tower 1 >> Area B`` signals. Pushing a Template Organizer Topic ---------------------------------- A template Organizer Topic can also be pushed, with specific content that you specify using a method that builds upon what you learned above. Since Organizer Topics are nearly always paired with a Workbench Analysis workbook that contains its content, you will be loading and pushing both in this example. We are going to re-use the template objects from the previous example, but this time we will push all of them instead of just the Workbench Analysis. A Single Topic Document with Multiple Assets ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In this first example, we will create a single Topic Document that has a separate *section* for each asset. .. code:: ipython3 workbooks = spy.workbooks.load('./Support Files/Workbook Templates.zip', as_template_with_label=f'{spy.user.username} Workbook Template Example 3') topic = workbooks['Topic Template'] analysis = workbooks['Analysis Template'] This Topic template looks like so in the Organizer user interface: .. image:: TopicTemplateScreenshot1.png Notice that there is some embedded content and there are some mustache variables. In particular, you can see an ``{{#Assets}}`` mustache designation, and if you were to scroll to the bottom, you would also see an ``{{/Assets}}`` entry too. This is a mustache *section*, which allows us to repeat a block of the document several times. (Note that if you want to exert more precise control over how your section is rendered – for example, if you want each section to be rendered by a ```` row in a table instead of as separate tables – you can use the ``pretty_print_html=True`` argument when you save the workbook templates, and then you can manually go into the saved HTML files and tweak where the Mustache tokens are placed.) As you’ll see below, we are going to have a block for each asset. .. code:: ipython3 document = topic.documents['Doc Template'] print(document.code) .. parsed-literal:: document.parameters = { "Document Name": None, "Assets": [ { "Asset Name": None, "25AF462C-602A-4E13-B50D-5DEB92CB37B5 [Embedded Content] Users >> mark-derbecker@seeq-com >> Workbook Templates >> Analysis Template >> Trend Template": None, "76B4412D-3B08-48B7-ABF9-ED8432DAA5CE [Embedded Content] Users >> mark-derbecker@seeq-com >> Workbook Templates >> Analysis Template >> XY Plot Template": None, "44392AAB-D443-4D2D-ACF0-055869875558 [Embedded Content] Users >> mark-derbecker@seeq-com >> Workbook Templates >> Analysis Template >> Treemap Template": None, "D4F167B3-E8BA-4FAA-86B3-05877E2D4250 [Embedded Content] Users >> mark-derbecker@seeq-com >> Workbook Templates >> Analysis Template >> Table Template": None } ] } Notice that the parameters code for the document contains an ``Assets`` entry, and it’s a list that includes entries for each of the pieces of embedded content (as well as for the mustache variable ``Asset Name``). We will specify a list of entries that are specific to each asset. First we need to create a set of content entries in the form of worksheet templates. Let’s look at the code for each worksheet: .. code:: ipython3 trend_template = analysis.worksheets['Trend Template'] xy_plot_template = analysis.worksheets['XY Plot Template'] treemap_template = analysis.worksheets['Treemap Template'] table_template = analysis.worksheets['Table Template'] print(trend_template.code) print(xy_plot_template.code) print(treemap_template.code) print(table_template.code) .. parsed-literal:: worksheet.parameters = { "D2C089B6-CE85-46FC-8392-E11CC0C08336 [Signal] Area A_Compressor Stage": None, "9564A6B8-8A8F-4F6D-AC63-00EA38962B7A [Signal] Area A_Temperature": None, "DCED9C36-A4BE-4783-9216-DC06B3F57D8C [Signal] Area A_Compressor Power": None, "Temperature Journal Link Text": None, "Compressor Power Journal Link Text": None, "Compressor Stage Journal Link Text": None, "favorite color": None } worksheet.parameters = { "9564A6B8-8A8F-4F6D-AC63-00EA38962B7A [Signal] Area A_Temperature": None, "DCED9C36-A4BE-4783-9216-DC06B3F57D8C [Signal] Area A_Compressor Power": None } worksheet.parameters = { "8A4F0E26-8A0C-4127-9E11-B67E031C6049 [Signal] Example >> Cooling Tower 1 >> Area A >> Temperature": None, "D16FC368-AE8E-47B4-B1A3-9C2A3FBA2BB6 [Asset] Example >> Cooling Tower 1": None, "4B40EAFC-91ED-4AB0-8199-F21AF40A8350 [Asset] Example >> Cooling Tower 1 >> Area A": None, "07F3161F-6644-4505-BC33-16D6155B004E [Condition] Hot": None } worksheet.parameters = { "D2C089B6-CE85-46FC-8392-E11CC0C08336 [Signal] Area A_Compressor Stage": None, "9564A6B8-8A8F-4F6D-AC63-00EA38962B7A [Signal] Area A_Temperature": None, "DCED9C36-A4BE-4783-9216-DC06B3F57D8C [Signal] Area A_Compressor Power": None } We’re going to create a separate set of worksheets *per asset*. We’ll do that by making a copy of each worksheet template in a ``for`` loop, and we’ll store them in a dictionary for later use. .. code:: ipython3 # We're going to reuse this code a couple of times, so we'll put it in a function def create_worksheets(analysis): trend_template = analysis.worksheets['Trend Template'] xy_plot_template = analysis.worksheets['XY Plot Template'] treemap_template = analysis.worksheets['Treemap Template'] table_template = analysis.worksheets['Table Template'] worksheet_templates = dict() for asset in ['Waveforms 1', 'Waveforms 2']: # We create a copy of the worksheets for each asset, and it must have a unique label. So we incorporate # the asset name into the label. unique_label = f'{spy.user.username} Workbook Template Example 3 - {asset}' trend_worksheet = trend_template.copy(unique_label) trend_worksheet.name = f'{asset} Trend' trend_worksheet.parameters = { "DCED9C36-A4BE-4783-9216-DC06B3F57D8C [Signal] Area A_Compressor Power": {'Asset': asset, 'Name': 'Sinusoid'}, "9564A6B8-8A8F-4F6D-AC63-00EA38962B7A [Signal] Area A_Temperature": {'Asset': asset, 'Name': 'Sawtooth'}, "D2C089B6-CE85-46FC-8392-E11CC0C08336 [Signal] Area A_Compressor Stage": {'Asset': asset, 'Name': 'Square'}, "Temperature Journal Link Text": 'Sawtooth', "Compressor Power Journal Link Text": 'Sinusoid', "Compressor Stage Journal Link Text": 'Square', "favorite color": 'eggshell' if asset.endswith('1') else 'violet' } xy_plot_worksheet = xy_plot_template.copy(unique_label) xy_plot_worksheet.name = f'{asset} XY Plot' xy_plot_worksheet.parameters = { "DCED9C36-A4BE-4783-9216-DC06B3F57D8C [Signal] Area A_Compressor Power": {'Asset': asset, 'Name': 'Sinusoid'}, "9564A6B8-8A8F-4F6D-AC63-00EA38962B7A [Signal] Area A_Temperature": {'Asset': asset, 'Name': 'Sawtooth'} } treemap_worksheet = treemap_template.copy(unique_label) treemap_worksheet.name = f'{asset} Treemap' treemap_worksheet.parameters = { "8A4F0E26-8A0C-4127-9E11-B67E031C6049 [Signal] Example >> Cooling Tower 1 >> Area A >> Temperature": { 'Asset': asset, 'Name': 'Sawtooth'}, "07F3161F-6644-4505-BC33-16D6155B004E [Condition] Hot": {'Asset': asset, 'Name': 'Hours'}, "D16FC368-AE8E-47B4-B1A3-9C2A3FBA2BB6 [Asset] Cooling Tower 1": {'Asset': 'Waveforms', 'Type': 'Asset'}, "4B40EAFC-91ED-4AB0-8199-F21AF40A8350 [Asset] Example >> Area A": {'Asset': asset, 'Type': 'Asset'} } table_worksheet = table_template.copy(unique_label) table_worksheet.name = f'{asset} Table' table_worksheet.parameters = { "DCED9C36-A4BE-4783-9216-DC06B3F57D8C [Signal] Area A_Compressor Power": {'Asset': asset, 'Name': 'Sinusoid'}, "D2C089B6-CE85-46FC-8392-E11CC0C08336 [Signal] Area A_Compressor Stage": {'Asset': asset, 'Name': 'Square'}, "9564A6B8-8A8F-4F6D-AC63-00EA38962B7A [Signal] Area A_Temperature": {'Asset': asset, 'Name': 'Sawtooth'} } worksheet_templates[asset] = { 'Trend': trend_worksheet, 'XY Plot': xy_plot_worksheet, 'Treemap': treemap_worksheet, 'Table': table_worksheet, } return worksheet_templates Now we will fill in the Organizer Topic document’s template parameters by using the worksheet templates that we assembled above. Note below that we are using a `list comprehension `__ to create the ``Assets`` list of each Waveform asset. .. code:: ipython3 worksheet_templates = create_worksheets(analysis) document.name = "Workbook Template Example 3" document.parameters = { "Document Name": "Waveform Monitoring and Diagnostics", "Assets": [ { "Asset Name": asset, "25AF462C-602A-4E13-B50D-5DEB92CB37B5 [Embedded Content] Users >> mark.derbecker@seeq.com >> Workbook Templates >> Analysis Template >> Trend Template": worksheet_templates[asset]['Trend'], "76B4412D-3B08-48B7-ABF9-ED8432DAA5CE [Embedded Content] Users >> mark.derbecker@seeq.com >> Workbook Templates >> Analysis Template >> XY Plot Template": worksheet_templates[asset]['XY Plot'], "44392AAB-D443-4D2D-ACF0-055869875558 [Embedded Content] Users >> mark.derbecker@seeq.com >> Workbook Templates >> Analysis Template >> Treemap Template": worksheet_templates[asset]['Treemap'], "D4F167B3-E8BA-4FAA-86B3-05877E2D4250 [Embedded Content] Users >> mark.derbecker@seeq.com >> Workbook Templates >> Analysis Template >> Table Template": worksheet_templates[asset]['Table'] } for asset in ['Waveforms 1', 'Waveforms 2'] ] } Now we do some final bookkeeping to tidy things up and push. Click on the link that corresponds to the Topic to see it in Organizer. .. code:: ipython3 # Give the topic and analysis a specific name topic.name = 'Workbook Template Example 3' analysis.name = 'Workbook Template Example 3' # Since each of the worksheets we created above is a copy of the template, the template # worksheets will show separately from the "Waveforms" worksheets. We want to "hide" the # template worksheets because it's often not interesting and perhaps confusing # for the user to see the templates. So we set the "Archived" property to True -- they # will still be pushed, but they'll be in the trash and the user won't see them. analysis.worksheets['Trend Template']['Archived'] = True analysis.worksheets['XY Plot Template']['Archived'] = True analysis.worksheets['Treemap Template']['Archived'] = True analysis.worksheets['Table Template']['Archived'] = True # Note that we pass in a list that includes the Analysis and the Topic pushed_df = spy.push(metadata=metadata_df, workbook=[topic, analysis]) # Show the inner Push Workbooks status object's display instead of the metadata push status, # since it has links to the Topic. pushed_df.spy.status.inner['Push Workbooks'].display() The Organizer Topic should have two sections, one for each Waveform asset, the second of which should look something like this: .. image:: TopicTemplateScreenshot2.png A Topic with a Document for each Asset ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now let’s see how it would look if you wanted a separate Topic Document for each Asset instead of putting them all in one Document. It looks very similar, but you’ll see that we use the ``copy()`` function to make separate documents. .. code:: ipython3 workbooks = spy.workbooks.load('./Support Files/Workbook Templates.zip', as_template_with_label=f'{spy.user.username} Workbook Template Example 4') topic = workbooks['Topic Template'] analysis = workbooks['Analysis Template'] worksheet_templates = create_worksheets(analysis) topic.name = 'Workbook Template Example 4' analysis.name = 'Workbook Template Example 4' # Grab the one and only document as a template. Note that it's name is left over from the previous example. doc_template = topic.documents['Doc Template'] for asset in ['Waveforms 1', 'Waveforms 2']: document = doc_template.copy(label=f'Document for {asset}') document.name = asset document.parameters = { "Document Name": "Waveform Monitoring and Diagnostics - v2", # This time, the Assets list is only one item long, since we're putting the Assets in different documents. "Assets": [ { "Asset Name": asset, "25AF462C-602A-4E13-B50D-5DEB92CB37B5 [Embedded Content] Users >> mark.derbecker@seeq.com >> Workbook Templates >> Analysis Template >> Trend Template": worksheet_templates[asset]['Trend'], "76B4412D-3B08-48B7-ABF9-ED8432DAA5CE [Embedded Content] Users >> mark.derbecker@seeq.com >> Workbook Templates >> Analysis Template >> XY Plot Template": worksheet_templates[asset]['XY Plot'], "44392AAB-D443-4D2D-ACF0-055869875558 [Embedded Content] Users >> mark.derbecker@seeq.com >> Workbook Templates >> Analysis Template >> Treemap Template": worksheet_templates[asset]['Treemap'], "D4F167B3-E8BA-4FAA-86B3-05877E2D4250 [Embedded Content] Users >> mark.derbecker@seeq.com >> Workbook Templates >> Analysis Template >> Table Template": worksheet_templates[asset]['Table'] } ] } # Since we have created a separate document for each asset, we don't want to see the template doc in the Topic doc_template['Archived'] = True pushed_df = spy.push(metadata=metadata_df, workbook=[topic, analysis]) pushed_df.spy.status.inner['Push Workbooks'].display() A Topic Document with an Image ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In the examples above, you saw how to use syntax like ``{{Asset Name}}`` as a placeholder in the template that became a parameter you could specify in your SPy code. You can do a similar thing with pictures/images in your document. The most common use for this capability is to push a custom visualization (that you created in Python) into your document, and typically you’ll want to do that on a schedule. A separate :doc:`Data Lab Visualizations ` notebook walks you through how to accomplish that.